﻿using Ardalis.GuardClauses;
using CleanArchitecture.Core.Configuration;
using CleanArchitecture.Core.Helper;
using CleanArchitecture.Infrastructure.Entities.AdminUsers;
using CleanArchitecture.Infrastructure.Entities.Customers;
using CleanArchitecture.Infrastructure.Entities.Settings;
using CleanArchitecture.Infrastructure.Interfaces.AdminUsers;
using CleanArchitecture.Infrastructure.Interfaces.Customers;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

namespace CleanArchitecture.Infrastructure.Services.Authentication
{
    /// <summary>
    /// Represents service using cookie middleware for the authentication
    /// </summary>
    public partial class AuthenticationService : Interfaces.Authentication.IAuthenticationService
    {
        #region Fields

        private readonly SecuritySettings _securitySettings;
        private readonly IAdminUserService _adminuserService;
        private readonly ICustomerService _customerService;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private AdminUser _cachedAdminUser;
        private Customer _cachedCustomer;

        #endregion

        #region Ctor

        public AuthenticationService(SecuritySettings securitySettings,
            IAdminUserService adminuserService,
            ICustomerService customerService,
            IHttpContextAccessor httpContextAccessor)
        {
            this._securitySettings = securitySettings;
            this._adminuserService = adminuserService;
            this._customerService = customerService;
            this._httpContextAccessor = httpContextAccessor;
        }

        #endregion

        #region Methods

        #region AdminUser Authentication
        /// <summary>
        /// Sign in
        /// </summary>
        /// <param name="customer">Customer</param>
        /// <param name="isPersistent">Whether the authentication session is persisted across multiple requests</param>
        public virtual async void SignIn(AdminUser user, bool isPersistent)
        {
            if (user == null)
                throw new ArgumentNullException(nameof(user));

            //create claims for customer's username and email
            var claims = new List<Claim>();

            if (!string.IsNullOrEmpty(user.UserLoginName))
                claims.Add(new Claim(ClaimTypes.Name, user.UserLoginName, ClaimValueTypes.String, AuthenticationDefaultConfig.ClaimsIssuer));

            if (!string.IsNullOrEmpty(user.Mobile))
                claims.Add(new Claim(ClaimTypes.MobilePhone, user.Mobile, ClaimValueTypes.String, AuthenticationDefaultConfig.ClaimsIssuer));

            if (!string.IsNullOrEmpty(user.Email))
                claims.Add(new Claim(ClaimTypes.Email, user.Email, ClaimValueTypes.Email, AuthenticationDefaultConfig.ClaimsIssuer));

            //create principal for the current authentication scheme
            var userIdentity = new ClaimsIdentity(claims, AuthenticationDefaultConfig.CookieAuthenticationScheme);
            var userPrincipal = new ClaimsPrincipal(userIdentity);
            DateTime authTime = DateTime.UtcNow;
            DateTime expiresAt = authTime.AddMinutes(Convert.ToDouble(AuthenticationDefaultConfig.CookieExpirationMinutes));
            //set value indicating whether session is persisted and the time at which the authentication was issued
            var authenticationProperties = new AuthenticationProperties
            {
                IsPersistent = isPersistent,
                IssuedUtc = authTime,
                ExpiresUtc = expiresAt
            };

            //sign in
            await _httpContextAccessor.HttpContext.SignInAsync(AuthenticationDefaultConfig.CookieAuthenticationScheme, userPrincipal, authenticationProperties);

            //cache authenticated customer
            _cachedAdminUser = user;
        }

        /// <summary>
        /// Sign out
        /// </summary>
        public virtual async void SignOut(SignInUserEnum userType = SignInUserEnum.AdminUser)
        {
            Guard.Against.Null(userType, nameof(userType));
            switch (userType)
            {
                case SignInUserEnum.Customer:
                    _cachedCustomer = null;
                    await _httpContextAccessor.HttpContext.SignOutAsync(AuthenticationDefaultConfig.JwtAuthenticationScheme);
                    break;
                case SignInUserEnum.AdminUser:
                default:
                    //reset cached customer
                    _cachedAdminUser = null;

                    //and sign out from the current authentication scheme
                    await _httpContextAccessor.HttpContext.SignOutAsync(AuthenticationDefaultConfig.CookieAuthenticationScheme);
                    break;
            }
        }

        /// <summary>
        /// Get authenticated customer
        /// </summary>
        /// <returns>Customer</returns>
        public virtual AdminUser GetAuthenticatedAdminUser()
        {
            //whether there is a cached customer
            if (_cachedAdminUser != null)
                return _cachedAdminUser;

            //try to get authenticated user identity
            var authenticateResult = _httpContextAccessor.HttpContext.AuthenticateAsync(AuthenticationDefaultConfig.CookieAuthenticationScheme).Result;
            if (!authenticateResult.Succeeded)
                return null;

            AdminUser adminuser = null;
            bool searchByUsername = (_securitySettings.AdminUserLogin.Count == 0
                                  || _securitySettings.AdminUserLogin.Contains(AdminUserLoginType.Loginname.ToString("F")))
                                  ? true : false;
            if (!searchByUsername)
            {
                if (_securitySettings.AdminUserLogin.Contains(AdminUserLoginType.Email.ToString("F")))
                {
                    //try to get customer by email
                    var emailClaim = authenticateResult.Principal.FindFirst(claim => claim.Type == ClaimTypes.Email
                        && claim.Issuer.Equals(AuthenticationDefaultConfig.ClaimsIssuer, StringComparison.InvariantCultureIgnoreCase));
                    if (emailClaim != null)
                        adminuser = _adminuserService.GetAdminUserByEmail(emailClaim.Value);
                }
                if (adminuser == null && _securitySettings.AdminUserLogin.Contains(AdminUserLoginType.Mobile.ToString("F")))
                {
                    //try to get customer by MobileNum
                    var mobileClaim = authenticateResult.Principal.FindFirst(claim => claim.Type == ClaimTypes.MobilePhone
                        && claim.Issuer.Equals(AuthenticationDefaultConfig.ClaimsIssuer, StringComparison.InvariantCultureIgnoreCase));
                    if (mobileClaim != null)
                        adminuser = _adminuserService.GetAdminUserByMobile(mobileClaim.Value);
                }
            }
            else
            {
                //try to get customer by username
                var usernameClaim = authenticateResult.Principal.FindFirst(claim => claim.Type == ClaimTypes.Name
                    && claim.Issuer.Equals(AuthenticationDefaultConfig.ClaimsIssuer, StringComparison.InvariantCultureIgnoreCase));
                if (usernameClaim != null)
                    adminuser = _adminuserService.GetAdminUserByLoginname(usernameClaim.Value);
            }

            //whether the found adminuser is available
            if (adminuser == null
             || adminuser.IsActive == Entities.EntityEnumConfig.LockState.InActive
             || adminuser.IsDeleted == Entities.EntityEnumConfig.DeleteState.Deleted)
                return null;

            //cache authenticated customer
            _cachedAdminUser = adminuser;

            return _cachedAdminUser;
        }
        #endregion
        #region Customer Authentication
        public Customer GetAuthenticatedCustomer()
        {
            //whether there is a cached customer
            if (_cachedCustomer != null)
                return _cachedCustomer;
             
            var authenticateResult = _httpContextAccessor.HttpContext.AuthenticateAsync(AuthenticationDefaultConfig.JwtAuthenticationScheme).Result;
            if (!authenticateResult.Succeeded)
                return null;
            //try to get customer by HashedId
            string ValidIssuer = string.IsNullOrEmpty(_securitySettings.JwtValidIssuer)
                               ? AuthenticationDefaultConfig.JwtValidIssuer
                               : _securitySettings.JwtValidIssuer;
            var customerhashClaim = authenticateResult.Principal.FindFirst(claim => claim.Type == ClaimTypes.Hash
                && claim.Issuer.Equals(ValidIssuer, StringComparison.InvariantCultureIgnoreCase));
            Customer customer = null;
            if (customerhashClaim != null)
                customer = _customerService.GetCustomerById(HashidsHelper.Decode(AuthenticationDefaultConfig.CustomerIdHashSalt, customerhashClaim.Value));
            //whether the found adminuser is available
            if (customer == null
             || customer.IsActive == Entities.EntityEnumConfig.LockState.InActive
             || customer.IsDeleted == Entities.EntityEnumConfig.DeleteState.Deleted)
                return null;

            //cache authenticated customer
            _cachedCustomer = customer;

            return _cachedCustomer;
        }

        public string SignIn(Customer customer)
        {
            if (customer == null)
                throw new ArgumentNullException(nameof(customer));

            var validIssuer = string.IsNullOrEmpty(_securitySettings.JwtValidIssuer)
                            ? AuthenticationDefaultConfig.JwtValidIssuer
                            : _securitySettings.JwtValidIssuer;
            var validAudience = string.IsNullOrEmpty(_securitySettings.JwtValidAudience)
                              ? AuthenticationDefaultConfig.JwtValidAudience
                              : _securitySettings.JwtValidAudience;
            var securityKey = string.IsNullOrEmpty(_securitySettings.JwtIssuerSigningKey)
                            ? AuthenticationDefaultConfig.JwtIssuerSigningKey
                            : _securitySettings.JwtIssuerSigningKey;
            var expireTime = _securitySettings.JwtExpirationMinutes <= 0
                           ? AuthenticationDefaultConfig.JwtExpirationMinutes
                           : _securitySettings.JwtExpirationMinutes;
            JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
            SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));

            DateTime authTime = DateTime.UtcNow;
            DateTime expiresAt = authTime.AddMinutes(Convert.ToDouble(expireTime));

            //将用户信息添加到 Claim 中
            var identity = new ClaimsIdentity(AuthenticationDefaultConfig.JwtAuthenticationScheme);
             
            IEnumerable<Claim> claims = new Claim[] {
                new Claim(ClaimTypes.Hash, HashidsHelper.Encode(AuthenticationDefaultConfig.CustomerIdHashSalt, customer.Id), ClaimValueTypes.String, AuthenticationDefaultConfig.JwtValidIssuer), 
                new Claim(ClaimTypes.Expiration, expiresAt.ToString(), ClaimValueTypes.DateTime, AuthenticationDefaultConfig.JwtValidIssuer)
            };
            identity.AddClaims(claims);
            var customerPrincipal = new ClaimsPrincipal(identity);
            //签发一个加密后的用户信息凭证，用来标识用户的身份
            _httpContextAccessor.HttpContext.SignInAsync(AuthenticationDefaultConfig.JwtAuthenticationScheme, customerPrincipal);

            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(claims),//创建声明信息
                Issuer = validIssuer,                //Jwt token 的签发者
                Audience = validAudience,            //Jwt token 的接收者
                Expires = expiresAt,                 //过期时间
                SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256)//创建 token
            };

            var token = tokenHandler.CreateToken(tokenDescriptor);
            //cache authenticated customer
            _cachedCustomer = customer;
            return tokenHandler.WriteToken(token);
        }
        #endregion

        #endregion
    }
}